package editor.animation;

import animation.*;
import com.crunch.math.Vector2f;
import com.crunch.math.Vector4f;

import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.filechooser.FileFilter;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;

public class SkinPanel extends EditorPanel {
	private JPanel skinComponentCollectionControlsPanel;
	private JButton loadSkinTextureButton;
	private JSpinner scaleSpinner;
	private JButton addSkinComponentButton;
	private JButton removeSkinComponentButton;
	private JTextField skinComponentNameField;
	private JList<String> skinComponentsList;
	private DefaultListModel<String> skinComponentsListModel;
	private JPanel skinComponentControlsPanel;

	private JSpinner originXSpinner;
	private JSpinner originYSpinner;
	private JSpinner boundMinXSpinner;
	private JSpinner boundMinYSpinner;
	private JSpinner boundMaxXSpinner;
	private JSpinner boundMaxYSpinner;

	private JFrame textureWindow;
	private JToggleButton gridButton;
	private JSpinner gridXSpinner;
	private JSpinner gridYSpinner;
	private SelectableImage selectableImage;

	private JPanel skinsControlsPanel;
	private JButton addSkinButton;
	private JButton removeSkinButton;
	private JTextField skinNameField;
	private JList<String> skinsList;
	private DefaultListModel<String> skinsListModel;

	private JPanel skinControlsPanel;
	private JTree boneTree;
	private DefaultTreeModel treeModel;
	private JButton clearButton;

	private boolean performCallbacks = true;

	private static final Vector4f ACTIVE_COLOR = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
	private static final Vector4f INACTIVE_COLOR = new Vector4f(0.5f, 0.5f, 0.5f, 1.0f);
	private static final Vector4f SELECTED_COLOR = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f);
	private static final Color ACTIVE_TEXT_COLOR = Color.BLACK;
	private static final Color INACTIVE_TEXT_COLOR = Color.GRAY;

	@Override
	public String getTabName() {
		return "Skins";
	}

	public SkinPanel(AnimationEditor editor) {
		this.editor = editor;

		JPanel mainPanelA = new JPanel();
		{
			GroupLayout topLayout = new GroupLayout(mainPanelA);
			mainPanelA.setLayout(topLayout);
			topLayout.setAutoCreateGaps(true);
			topLayout.setAutoCreateContainerGaps(true);

			// skin component set
			// component controls
			// skins
			// bone list

			skinComponentCollectionControlsPanel = new JPanel();
			skinComponentCollectionControlsPanel.setBorder(BorderFactory.createEtchedBorder());
			{
				GroupLayout layout = new GroupLayout(skinComponentCollectionControlsPanel);
				skinComponentCollectionControlsPanel.setLayout(layout);
				layout.setAutoCreateGaps(true);
				layout.setAutoCreateContainerGaps(true);

				loadSkinTextureButton = new JButton("Load texture");
				loadSkinTextureButton.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						synchronized (SkinPanel.this.editor) {
							loadSkinTexture();
						}
					}
				});

				final int SCALE_WIDTH = 64;
				JLabel preScaleLabel = new JLabel("Scale: 1 unit =");
				scaleSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(0.0f), new Float(100000.0f), new Float(1.0f)));
				scaleSpinner.setMaximumSize(new Dimension(SCALE_WIDTH, scaleSpinner.getMaximumSize().height));
				scaleSpinner.addChangeListener(new ChangeListener() {
					@Override
					public void stateChanged(ChangeEvent e) {
						synchronized (SkinPanel.this.editor) {
							if (!performCallbacks) {
								return;
							}
							storeSkinComponentSetProperties();
						}
					}
				});
				JLabel postScaleLabel = new JLabel("pixels");

				addSkinComponentButton = new JButton("Add component");
				addSkinComponentButton.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						synchronized (SkinPanel.this.editor) {
							addSkinComponent();
						}
					}
				});

				removeSkinComponentButton = new JButton("Remove component");
				removeSkinComponentButton.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						synchronized (SkinPanel.this.editor) {
							removeSkinComponent();
						}
					}
				});

				JLabel nameLabel = new JLabel("Name:", JLabel.TRAILING);
				skinComponentNameField = new JTextField();
				skinComponentNameField.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						synchronized (SkinPanel.this.editor) {
							if (!performCallbacks) {
								return;
							}
							storeSkinComponentProperties();
						}
					}
				});
				skinComponentNameField.addFocusListener(new FocusListener() {
					@Override
					public void focusGained(FocusEvent e) {}

					@Override
					public void focusLost(FocusEvent e) {
						synchronized (SkinPanel.this.editor) {
							if (!performCallbacks) {
								return;
							}
							storeSkinComponentProperties();
						}
					}
				});
				nameLabel.setLabelFor(skinComponentNameField);

				skinComponentsList = new JList<String>(new DefaultListModel<String>());
				skinComponentsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
				skinComponentsListModel = (DefaultListModel<String>) skinComponentsList.getModel();
				skinComponentsList.addListSelectionListener(new ListSelectionListener() {
					@Override
					public void valueChanged(ListSelectionEvent e) {
						synchronized (SkinPanel.this.editor) {
							if (!performCallbacks) {
								return;
							}

							int sel = -1;
							for (int i = e.getFirstIndex(); sel == -1 && i <= e.getLastIndex() && i < skinComponentsListModel.size(); ++i) {
								if (skinComponentsList.isSelectedIndex(i)) {
									sel = i;
								}
							}

							storeSkinComponentProperties();
							skinComponentSelection = sel;
							setSkinComponentControlsEnabled(sel >= 0);
							if (sel >= 0) {
								loadSkinComponentProperties();
							}

							// selecting from the list sets the bone's skin component
							if (boneSelection >= 0) {
								storeBoneProperties();
							}
						}
					}
				});
				JScrollPane skinComponentsListScroll = new JScrollPane(skinComponentsList);

				layout.setHorizontalGroup(layout.createParallelGroup()
						.addGroup(layout.createSequentialGroup()
								.addComponent(loadSkinTextureButton)
								.addComponent(preScaleLabel)
								.addComponent(scaleSpinner)
								.addComponent(postScaleLabel))
						.addGroup(layout.createSequentialGroup()
								.addComponent(addSkinComponentButton)
								.addComponent(removeSkinComponentButton)
								.addComponent(nameLabel)
								.addComponent(skinComponentNameField))
						.addComponent(skinComponentsListScroll));
				layout.setVerticalGroup(layout.createSequentialGroup()
						.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
								.addComponent(loadSkinTextureButton)
								.addComponent(preScaleLabel)
								.addComponent(scaleSpinner)
								.addComponent(postScaleLabel))
						.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
								.addComponent(addSkinComponentButton)
								.addComponent(removeSkinComponentButton)
								.addComponent(nameLabel)
								.addComponent(skinComponentNameField))
						.addComponent(skinComponentsListScroll));
			}

			skinComponentControlsPanel = new JPanel();
			skinComponentControlsPanel.setBorder(BorderFactory.createEtchedBorder());
			{
				GroupLayout layout = new GroupLayout(skinComponentControlsPanel);
				skinComponentControlsPanel.setLayout(layout);
				layout.setAutoCreateGaps(true);
				layout.setAutoCreateContainerGaps(true);

				ChangeListener changeListener = new ChangeListener() {
					@Override
					public void stateChanged(ChangeEvent e) {
						synchronized (SkinPanel.this.editor) {
							if (!performCallbacks) {
								return;
							}
							storeSkinComponentProperties();
						}
					}
				};

				final int MIN_TEXT_WIDTH = 64;

				JLabel originLabel = new JLabel("Origin:");
				originXSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(-100000.0f), new Float(100000.0f), new Float(1.0f)));
				originXSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, originXSpinner.getMinimumSize().height));
				originXSpinner.addChangeListener(changeListener);
				originYSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(-100000.0f), new Float(100000.0f), new Float(1.0f)));
				originYSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, originYSpinner.getMinimumSize().height));
				originYSpinner.addChangeListener(changeListener);
				originLabel.setLabelFor(originXSpinner);

				JLabel boundMinLabel = new JLabel("Min bound:");
				boundMinXSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(-100000.0f), new Float(100000.0f), new Float(1.0f)));
				boundMinXSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, boundMinXSpinner.getMinimumSize().height));
				boundMinXSpinner.addChangeListener(changeListener);
				boundMinYSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(-100000.0f), new Float(100000.0f), new Float(1.0f)));
				boundMinYSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, boundMinYSpinner.getMinimumSize().height));
				boundMinYSpinner.addChangeListener(changeListener);
				boundMinLabel.setLabelFor(boundMinXSpinner);

				JLabel boundMaxLabel = new JLabel("Max bound:");
				boundMaxXSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(-100000.0f), new Float(100000.0f), new Float(1.0f)));
				boundMaxXSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, boundMaxXSpinner.getMinimumSize().height));
				boundMaxXSpinner.addChangeListener(changeListener);
				boundMaxYSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(-100000.0f), new Float(100000.0f), new Float(1.0f)));
				boundMaxYSpinner.setMinimumSize(new Dimension(MIN_TEXT_WIDTH, boundMaxYSpinner.getMinimumSize().height));
				boundMaxYSpinner.addChangeListener(changeListener);
				boundMaxLabel.setLabelFor(boundMaxXSpinner);

				layout.setHorizontalGroup(layout.createSequentialGroup()
						.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
								.addComponent(originLabel)
								.addComponent(boundMinLabel)
								.addComponent(boundMaxLabel))
						.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
								.addComponent(originXSpinner)
								.addComponent(boundMinXSpinner)
								.addComponent(boundMaxXSpinner))
						.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
								.addComponent(originYSpinner)
								.addComponent(boundMinYSpinner)
								.addComponent(boundMaxYSpinner)));
				layout.setVerticalGroup(layout.createSequentialGroup()
						.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
								.addComponent(originLabel)
								.addComponent(originXSpinner)
								.addComponent(originYSpinner))
						.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
								.addComponent(boundMinLabel)
								.addComponent(boundMinXSpinner)
								.addComponent(boundMinYSpinner))
						.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
								.addComponent(boundMaxLabel)
								.addComponent(boundMaxXSpinner)
								.addComponent(boundMaxYSpinner)));
			}

			topLayout.setHorizontalGroup(topLayout.createParallelGroup()
					.addComponent(skinComponentCollectionControlsPanel)
					.addComponent(skinComponentControlsPanel));
			topLayout.setVerticalGroup(topLayout.createSequentialGroup()
					.addComponent(skinComponentCollectionControlsPanel)
					.addComponent(skinComponentControlsPanel));
		}

		JPanel mainPanelB = new JPanel();
		{
			GroupLayout topLayout = new GroupLayout(mainPanelB);
			mainPanelB.setLayout(topLayout);
			topLayout.setAutoCreateGaps(true);
			topLayout.setAutoCreateContainerGaps(true);
			{
				textureWindow = new JFrame();
				textureWindow.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
				GroupLayout windowLayout = new GroupLayout(textureWindow.getContentPane());
				textureWindow.getContentPane().setLayout(windowLayout);

				gridButton = new JToggleButton("Grid");
				gridButton.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						selectableImage.setGridEnabled(gridButton.isSelected());
					}
				});

				JPanel gridControlsPanel = new JPanel();
				gridControlsPanel.setBorder(BorderFactory.createEtchedBorder());
				GroupLayout layout = new GroupLayout(gridControlsPanel);
				gridControlsPanel.setLayout(layout);
				layout.setAutoCreateGaps(true);
				layout.setAutoCreateContainerGaps(true);

				final int WIDTH = 64;

				ChangeListener changeListener = new ChangeListener() {
					@Override
					public void stateChanged(ChangeEvent e) {
						selectableImage.setGridSize(
								(Integer) gridXSpinner.getValue(),
								(Integer) gridYSpinner.getValue());
					}
				};

				JLabel gridXLabel = new JLabel("X:");
				gridXSpinner = new JSpinner(new SpinnerNumberModel(16, 0, 1000, 1));
				gridXSpinner.setMaximumSize(new Dimension(WIDTH, gridXSpinner.getMinimumSize().height));
				gridXSpinner.addChangeListener(changeListener);
				gridXLabel.setLabelFor(gridXSpinner);
				JLabel gridYLabel = new JLabel("Y:");
				gridYSpinner = new JSpinner(new SpinnerNumberModel(16, 0, 1000, 1));
				gridYSpinner.setMaximumSize(new Dimension(WIDTH, gridYSpinner.getMaximumSize().height));
				gridYSpinner.addChangeListener(changeListener);
				gridYLabel.setLabelFor(gridYSpinner);

				layout.setHorizontalGroup(layout.createSequentialGroup()
						.addComponent(gridButton)
						.addComponent(gridXLabel)
						.addComponent(gridXSpinner)
						.addComponent(gridYLabel)
						.addComponent(gridYSpinner));
				layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
						.addComponent(gridButton)
						.addComponent(gridXLabel)
						.addComponent(gridXSpinner)
						.addComponent(gridYLabel)
						.addComponent(gridYSpinner));

				selectableImage = new SelectableImage();
				selectableImage.setSelectionListener(new SelectableImage.SelectionListener() {
					@Override
					public void onRectangleSelected(int x0, int y0, int x1, int y1) {
						synchronized (SkinPanel.this.editor) {
							boundMinXSpinner.setValue((float) x0);
							boundMinYSpinner.setValue((float) y0);
							boundMaxXSpinner.setValue((float) x1);
							boundMaxYSpinner.setValue((float) y1);
						}
					}

					@Override
					public void onCrossSelected(int x, int y) {
						synchronized (SkinPanel.this.editor) {
							originXSpinner.setValue((float) x);
							originYSpinner.setValue((float) y);
						}
					}
				});
				JScrollPane scroll = new JScrollPane(selectableImage);

				windowLayout.setHorizontalGroup(windowLayout.createParallelGroup()
						.addComponent(gridControlsPanel)
						.addComponent(scroll));
				windowLayout.setVerticalGroup(windowLayout.createSequentialGroup()
						.addComponent(gridControlsPanel)
						.addComponent(scroll));

				selectableImage.setGridEnabled(gridButton.isSelected());
				selectableImage.setGridSize((Integer) gridXSpinner.getValue(), (Integer) gridYSpinner.getValue());

				textureWindow.pack();
				int minWidth = textureWindow.getWidth();
				textureWindow.setMinimumSize(new Dimension(minWidth, textureWindow.getMinimumSize().height));
				textureWindow.pack();
			}

			skinsControlsPanel = new JPanel();
			skinsControlsPanel.setBorder(BorderFactory.createEtchedBorder());
			{
				GroupLayout layout = new GroupLayout(skinsControlsPanel);
				skinsControlsPanel.setLayout(layout);
				layout.setAutoCreateGaps(true);
				layout.setAutoCreateContainerGaps(true);

				addSkinButton = new JButton("Add skin");
				addSkinButton.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						synchronized (SkinPanel.this.editor) {
							addSkin();
						}
					}
				});
				removeSkinButton = new JButton("Remove skin");
				removeSkinButton.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						synchronized (SkinPanel.this.editor) {
							removeSkin();
						}
					}
				});

				JLabel nameLabel = new JLabel("Name:", JLabel.TRAILING);
				skinNameField = new JTextField();
				skinNameField.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						synchronized (SkinPanel.this.editor) {
							if (!performCallbacks) {
								return;
							}
							storeSkinProperties();
						}
					}
				});
				skinNameField.addFocusListener(new FocusListener() {
					@Override
					public void focusGained(FocusEvent e) {}

					@Override
					public void focusLost(FocusEvent e) {
						synchronized (SkinPanel.this.editor) {
							if (!performCallbacks) {
								return;
							}
							storeSkinProperties();
						}
					}
				});
				nameLabel.setLabelFor(skinNameField);

				skinsList = new JList<String>(new DefaultListModel<String>());
				skinsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
				skinsListModel = (DefaultListModel<String>) skinsList.getModel();
				skinsList.addListSelectionListener(new ListSelectionListener() {
					@Override
					public void valueChanged(ListSelectionEvent e) {
						synchronized (SkinPanel.this.editor) {
							if (!performCallbacks) {
								return;
							}

							int sel = -1;
							for (int i = e.getFirstIndex(); sel == -1 && i <= e.getLastIndex() && i < skinsListModel.size(); ++i) {
								if (skinsList.isSelectedIndex(i)) {
									sel = i;
								}
							}

							storeBoneProperties();
							boneSelection = -1;
							boolean tempPerformCallbacks = performCallbacks;
							performCallbacks = false;
							try {
								boneTree.clearSelection();
							} finally {
								performCallbacks = tempPerformCallbacks;
							}
							setBoneControlsEnabled(false);

							storeSkinProperties();
							skinSelection = sel;
							setSkinControlsEnabled(sel >= 0);
							if (sel >= 0) {
								loadSkinProperties();
							}

							SkinPanel.this.editor.setSkinSelection(sel);
						}
					}
				});

				JScrollPane skinsListScroll = new JScrollPane(skinsList);
				skinsListScroll.setMinimumSize(new Dimension(skinsListScroll.getMinimumSize().width, 64));
				skinsListScroll.setMaximumSize(new Dimension(skinsListScroll.getMaximumSize().width, 128));

				layout.setHorizontalGroup(layout.createParallelGroup()
						.addGroup(layout.createSequentialGroup()
								.addComponent(addSkinButton)
								.addComponent(removeSkinButton)
								.addComponent(nameLabel)
								.addComponent(skinNameField))
						.addComponent(skinsListScroll));
				layout.setVerticalGroup(layout.createSequentialGroup()
						.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
								.addComponent(addSkinButton)
								.addComponent(removeSkinButton)
								.addComponent(nameLabel)
								.addComponent(skinNameField))
						.addComponent(skinsListScroll));
			}

			skinControlsPanel = new JPanel();
			skinControlsPanel.setBorder(BorderFactory.createEtchedBorder());
			{
				GroupLayout layout = new GroupLayout(skinControlsPanel);
				skinControlsPanel.setLayout(layout);
				layout.setAutoCreateGaps(true);
				layout.setAutoCreateContainerGaps(true);

				clearButton = new JButton("Clear bone");
				clearButton.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						synchronized (SkinPanel.this.editor) {
							// deselect skin component list
							skinComponentsList.clearSelection();
						}
					}
				});

				boneTree = new JTree(new ColoredNode("skeleton"));
				boneTree.setCellRenderer(new ColoredNodeRenderer());
				treeModel = (DefaultTreeModel) boneTree.getModel();
				boneTree.setShowsRootHandles(true);
				boneTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
				boneTree.addTreeSelectionListener(new TreeSelectionListener() {
					@Override
					public void valueChanged(TreeSelectionEvent e) {
						synchronized (SkinPanel.this.editor) {
							if (!performCallbacks) {
								return;
							}

							TreePath path = e.getNewLeadSelectionPath();
							ColoredNode node = (path == null || path.getLastPathComponent() == treeModel.getRoot()) ?
									null : (ColoredNode) path.getLastPathComponent();
							int sel;
							if (node == null) {
								sel = -1;
							} else {
								sel = SkinPanel.this.editor.skeleton.getBoneIndex(node.toString());
							}

							storeBoneProperties();
							boneSelection = sel;
							setBoneControlsEnabled(sel >= 0);
							if (sel >= 0) {
								loadBoneProperties();
							}

							updateBoneColors();
						}
					}
				});
				JScrollPane boneTreeScroll = new JScrollPane(boneTree);

				layout.setHorizontalGroup(layout.createParallelGroup()
						.addComponent(clearButton)
						.addComponent(boneTreeScroll));
				layout.setVerticalGroup(layout.createSequentialGroup()
						.addComponent(clearButton)
						.addComponent(boneTreeScroll));
			}

			topLayout.setHorizontalGroup(topLayout.createParallelGroup()
					.addComponent(skinsControlsPanel)
					.addComponent(skinControlsPanel));
			topLayout.setVerticalGroup(topLayout.createSequentialGroup()
					.addComponent(skinsControlsPanel)
					.addComponent(skinControlsPanel));
		}

		setLayout(new BorderLayout());
		JSplitPane topPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, mainPanelA, mainPanelB);
		add(topPane, BorderLayout.CENTER);
	}

	@Override
	public void select() {
		textureWindow.setVisible(true);
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			// clear the tree and rebuild it up
			boneTree.clearSelection();
			ColoredNode root = (ColoredNode) treeModel.getRoot();
			root.removeAllChildren();
			treeModel.reload();

			for (int i = 0; i < editor.skeleton.getRootBoneCount(); ++i) {
				addTreeNodes(editor.skeleton.getRootBone(i), root);
			}

			// clear the components list and build it up
			skinComponentsList.clearSelection();
			skinComponentsListModel.clear();

			// add components
			for (int i = 0; i < editor.skinComponentCollection.getComponentCount(); ++i) {
				skinComponentsListModel.addElement(editor.skinComponentCollection.getComponent(i).getName());
			}

			// clear the skins list and build it up
			skinsList.clearSelection();
			skinsListModel.clear();

			// add skins
			for (int i = 0; i < editor.skins.size(); ++i) {
				skinsListModel.addElement(editor.skinNames.get(i));
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}

		// expand the whole tree - note, this is probably n^2 due to the getRowCount() call in the loop
		for (int i = 0; i < boneTree.getRowCount(); ++i) {
			boneTree.expandRow(i);
		}

		loadSkinComponentSetProperties();
		refreshSelectionWindow();

		skinComponentSelection = -1;
		setSkinComponentControlsEnabled(false);
		skinSelection = -1;
		setSkinControlsEnabled(false);
		boneSelection = -1;
		setBoneControlsEnabled(false);

		constructCharacterInstance();

		if (editor.skinSelection >= 0) {
			skinsList.setSelectedIndex(editor.skinSelection);
		}
	}

	@Override
	public void deselect() {
		textureWindow.setVisible(false);
	}

	@Override
	public void close() {
		textureWindow.dispose();
	}

	private void addTreeNodes(int idx, ColoredNode parent) {
		Skeleton.Bone bone = editor.skeleton.getBone(idx);

		ColoredNode node = new ColoredNode(bone.getName());
		treeModel.insertNodeInto(node, parent, parent.getChildCount());

		for (int i = 0; i < bone.getChildCount(); ++i) {
			addTreeNodes(bone.getChild(i), node);
		}
	}

	public void loadSkinTexture() {
		JFileChooser chooser = new JFileChooser();
		chooser.addChoosableFileFilter(new FileFilter() {
			@Override
			public boolean accept(File f) {
				if (f.isDirectory()) {
					return false;
				}
				String upperName = f.getName().toUpperCase();
				return upperName.endsWith("PNG") ||
						upperName.endsWith("BMP") ||
						upperName.endsWith("TGA") ||
						upperName.endsWith("JPG") ||
						upperName.endsWith("JPEG");
			}

			@Override
			public String getDescription() {
				return "Image files (PNG, BMP, TGA, JPG)";
			}
		});
		int result = chooser.showOpenDialog(this);
		if (result != JFileChooser.APPROVE_OPTION) {
			return;
		}

		String fname = chooser.getSelectedFile().getAbsolutePath();
		try {
			editor.skinImage = ImageIO.read(new File(fname));
		} catch (IOException e) {
			System.out.println("Failed to load image");
			return;
		}
		editor.refreshSkinImage = true;

		refreshSelectionWindow();
	}

	private void refreshSelectionWindow() {
		selectableImage.setImage(editor.skinImage);
		if (editor.skinImage == null) {
			selectableImage.getParent().setPreferredSize(new Dimension(0, 0));
		} else {
			selectableImage.getParent().setPreferredSize(new Dimension(editor.skinImage.getWidth(),
					editor.skinImage.getHeight()));
		}
		textureWindow.pack();
	}

	private int skinComponentSelection; // ID of selected skin component
	private int skinSelection;          // ID of selected skin
	private int boneSelection;          // ID of selected bone

	private void storeSkinComponentSetProperties() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			// scale
			editor.skinComponentCollection.setScale((Float) scaleSpinner.getValue());
		} finally {
			performCallbacks = tempPerformCallbacks;
		}

		// construct character
	}

	private void loadSkinComponentSetProperties() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			// scale
			scaleSpinner.setValue(editor.skinComponentCollection.getScale());
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void addSkinComponent() {
		int val = 0;
		// find unused component name
		boolean used = true;
		while (used) {
			if (!skinComponentsListModel.contains("comp" + val)) {
				used = false;
			} else {
				++val;
			}
		}

		String name = "comp" + val;

		// create a component
		editor.skinComponentCollection.addComponent(name);

		// add and select the component
		skinComponentsListModel.addElement(name);
		editor.setSkinSelection(editor.skinSelection);
		skinComponentsList.setSelectedIndex(skinComponentsListModel.getSize()-1);
	}

	private void removeSkinComponent() {
		int sel = skinComponentSelection;

		// deselect and remove the component
		skinComponentsList.clearSelection();

		editor.skinComponentCollection.removeComponent(sel);
		skinComponentsListModel.remove(sel);

		editor.setSkinSelection(editor.skinSelection);
	}

	private void storeSkinComponentProperties() {
		if (skinComponentSelection < 0) {
			return;
		}
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			SkinComponentCollection.Component component = editor.skinComponentCollection.getComponent(skinComponentSelection);

			// name
			String name = skinComponentNameField.getText();
			if (name.isEmpty() || editor.skinComponentCollection.getComponentIndex(name) >= 0) {
				// revert to current component name
				skinComponentNameField.setText(editor.skinComponentCollection.getComponent(skinComponentSelection)
						.getName());
			} else {
				// set component's name
				editor.skinComponentCollection.setComponentName(skinComponentSelection, name);
				skinComponentsListModel.set(skinComponentSelection, name);
			}

			component.setOrigin(new Vector2f(
					(Float) originXSpinner.getValue(),
					(Float) originYSpinner.getValue()));
			component.setBoundMin(new Vector2f(
					(Float) boundMinXSpinner.getValue(),
					(Float) boundMinYSpinner.getValue()));
			component.setBoundMax(new Vector2f(
					(Float) boundMaxXSpinner.getValue(),
					(Float) boundMaxYSpinner.getValue()));
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void loadSkinComponentProperties() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			SkinComponentCollection.Component component = editor.skinComponentCollection.getComponent(skinComponentSelection);
			skinComponentNameField.setText(component.getName());
			originXSpinner.setValue(component.getOrigin().x());
			originYSpinner.setValue(component.getOrigin().y());
			boundMinXSpinner.setValue(component.getBoundMin().x());
			boundMinYSpinner.setValue(component.getBoundMin().y());
			boundMaxXSpinner.setValue(component.getBoundMax().x());
			boundMaxYSpinner.setValue(component.getBoundMax().y());

			selectableImage.setCrossSelection(
					(int) component.getOrigin().x(),
					(int) component.getOrigin().y(), false);
			selectableImage.setRectangleSelection(
					(int) component.getBoundMin().x(),
					(int) component.getBoundMin().y(),
					(int) component.getBoundMax().x(),
					(int) component.getBoundMax().y(), false);
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void setSkinComponentControlsEnabled(boolean e) {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			skinComponentNameField.setEnabled(e);
			removeSkinComponentButton.setEnabled(e);
			originXSpinner.setEnabled(e);
			originYSpinner.setEnabled(e);
			boundMinXSpinner.setEnabled(e);
			boundMinYSpinner.setEnabled(e);
			boundMaxXSpinner.setEnabled(e);
			boundMaxYSpinner.setEnabled(e);

			selectableImage.setSelectingEnabled(e);
			if (!e) {
				selectableImage.clearRectangleSelection();
				selectableImage.clearCrossSelection();
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void addSkin() {
		int val = 0;
		// find unused skin name
		boolean used = true;
		while (used) {
			if (!skinsListModel.contains("skin" + val)) {
				used = false;
			} else {
				++val;
			}
		}

		String name = "skin" + val;

		// create a skin
		editor.skinNames.add(name);
		Skin skin = new Skin();
		editor.skins.add(skin);

		// add and select the skin
		skinsListModel.addElement(name);
		skinsList.setSelectedIndex(skinsListModel.getSize()-1);
	}

	private void removeSkin() {
		int sel = skinSelection;

		// deselect and remove the skin
		skinsList.clearSelection();

		editor.skinNames.remove(sel);
		editor.skins.remove(sel);
		skinsListModel.remove(sel);
	}

	private void storeSkinProperties() {
		if (skinSelection < 0) {
			return;
		}
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			// name
			String name = skinNameField.getText();
			if (name.isEmpty() || editor.skinNames.contains(name)) {
				// revert to current skin name
				skinNameField.setText(editor.skinNames.get(skinSelection));
			} else {
				// set skin's name
				editor.skinNames.set(skinSelection, name);
				skinsListModel.set(skinSelection, name);
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void loadSkinProperties() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			skinNameField.setText(editor.skinNames.get(skinSelection));
			setBoneTreeTextColors();
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void setSkinControlsEnabled(boolean e) {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			skinNameField.setEnabled(e);
			removeSkinButton.setEnabled(e);
			boneTree.setEnabled(e);
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void storeBoneProperties() {
		if (boneSelection < 0) {
			return;
		}
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			Skin skin = editor.skins.get(skinSelection);
			Skin.SkinnedBone bone = skin.getBone(editor.skeleton.getBone(boneSelection).getName());

			boolean skinned = skinComponentSelection >= 0;
			if (skinned && bone == null) {
				// add the bone
				skin.addSkinnedBone(editor.skeleton.getBone(boneSelection).getName(),
						editor.skinComponentCollection.getComponent(skinComponentSelection).getName());
				bone = skin.getBone(editor.skeleton.getBone(boneSelection).getName());
			} if (!skinned && bone != null) {
				// remove the bone
				skin.removeSkinnedBone(editor.skeleton.getBone(boneSelection).getName());
			}
			if (skinned) {
				bone.setComponentName(editor.skinComponentCollection.getComponent(skinComponentSelection).getName());
			}
			setBoneTreeTextColors();
		} finally {
			performCallbacks = tempPerformCallbacks;
		}

		editor.setSkinSelection(editor.skinSelection);
	}

	private void loadBoneProperties() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			Skin skin = editor.skins.get(skinSelection);
			Skin.SkinnedBone bone = skin.getBone(editor.skeleton.getBone(boneSelection).getName());

			if (bone == null) {
				performCallbacks = true;
				try {
					skinComponentsList.clearSelection();
				} finally {
					performCallbacks = false;
				}
			} else {
				int i;
				for (i = 0; i < editor.skinComponentCollection.getComponentCount(); ++i) {
					if (editor.skinComponentCollection.getComponent(i).getName().equals(bone.getComponentName())) {
						break;
					}
				}

				performCallbacks = true;
				try {
					skinComponentsList.setSelectedIndex(i);
				} finally {
					performCallbacks = false;
				}
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void setBoneControlsEnabled(boolean e) {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			clearButton.setEnabled(e);
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	public void constructCharacterInstance() {
		// no need to construct animations for skeleton modification
		//HashMap<String, Animation> animMap = new HashMap<String, Animation>();
		//for (int i = 0; i < editor.animations.size(); ++i) {
		//	animMap.put(editor.animationNames.get(i), editor.animations.get(i));
		//}
		editor.character = new animation.Character(
				editor.skeleton,
				new AnimationTree(),            // empty tree
				new AnimationStateMachine(),    // empty state machine
				new HashMap<String, Animation>());
		editor.state = editor.character.createCharacterState();
		editor.evaluator.evaluate(editor.state);
		editor.boneColors = new Vector4f[editor.skeleton.getBoneCount()];
		updateBoneColors();
	}

	private void updateBoneColors() {
		for (int i = 0; i < editor.boneColors.length; ++i) {
			editor.boneColors[i] = INACTIVE_COLOR;
		}
		if (skinSelection >= 0) {
			Skin skin= editor.skins.get(skinSelection);
			for (int i = 0; i < skin.getSkinnedBoneCount(); ++i) {
				String bone = skin.getSkinnedBone(i).getBoneName();
				int boneIndex = editor.skeleton.getBoneIndex(bone);
				// check for invalid bones
				if (boneIndex >= 0) {
					editor.boneColors[boneIndex] = ACTIVE_COLOR;
				}
			}
		}
		if (boneSelection >= 0) {
			editor.boneColors[boneSelection] = SELECTED_COLOR;
		}
	}

	private void setBoneTreeTextColors() {
		ColoredNode root = (ColoredNode) treeModel.getRoot();
		@SuppressWarnings("unchecked")
		Enumeration<ColoredNode> enumeration = root.preorderEnumeration();
		enumeration.nextElement(); // skip root

		Skin skin = editor.skins.get(skinSelection);
		while (enumeration.hasMoreElements()) {
			ColoredNode node = enumeration.nextElement();
			String name = node.toString();
			if (skin.getBone(name) != null) {
				node.setColor(ACTIVE_TEXT_COLOR);
			} else {
				node.setColor(INACTIVE_TEXT_COLOR);
			}
		}
		boneTree.repaint();
	}

	private final AnimationEditor editor;

	public void selectBoneInPreview(int boneIndex) {
		synchronized (editor) {
			if (skinSelection < 0) {
				return;
			}
			if (boneIndex < 0) {
				// deselect
				boneTree.clearSelection();
			} else {
				// make sure the bone still exists
				if (boneIndex >= editor.skeleton.getBoneCount())
					return;

				String boneName = editor.skeleton.getBone(boneIndex).getName();
				// find the bone in the list
				@SuppressWarnings("unchecked")
				Enumeration<DefaultMutableTreeNode> enumeration =
						((DefaultMutableTreeNode) treeModel.getRoot()).preorderEnumeration();
				enumeration.nextElement(); // skip root
				while (enumeration.hasMoreElements()) {
					DefaultMutableTreeNode node = enumeration.nextElement();
					if (node.toString().equals(boneName)) {
						boneTree.setSelectionPath(new TreePath(node.getPath()));
						break;
					}
				}
			}
		}
	}

	public void translateInPreview(Vector2f translation) {}
	public void finishTranslateInPreview() {}
	public void rotateInPreview(float rotation) {}
	public void finishRotateInPreview() {}
	public void scaleInPreview(Vector2f scale) {}
	public void finishScaleInPreview() {}
	public Vector2f getBoneTranslation(int boneIndex) { return null; }
	public float getBoneRotation(int boneIndex) { return 0.0f; }
	public Vector2f getBoneScale(int boneIndex) { return null; }
	public void enableBoneInAnimationInPreview() {}
	public void addKeyframeInPreview() {}
	public void prevKeyframeInPreview(boolean first) {}
	public void nextKeyframeInPreview(boolean last) {}
	public void frameBeginInPreview() {}
}
